﻿
window.SubtractJS = (($) ->
    isInitialized = false;
    doBodyInitialize = true;
    isResizing = false;
    isSubtractJSPaused = false;
    beforeResize = =>
    prefix = "sj";
    baseClassName = prefix + "-fill";
    fillerSelector = "";
    widthElementsSelector = "";
    heightElementsSelector = "";
    elementsSelector = "";
    selectorList = [];
    nextParentId = 0;
    constFixed = "fixed";
    constFill = "fill";
    
    constWidth = "width";
    constHeight = "height";
    constLeft = "left";
    constTop = "top";
    constOverflow = "overflow";
    constHidden = "hidden";
    
    beginCss = {};
    beginCss[constWidth] = "";
    beginCss[constHeight] = "";
    beginCss[constLeft] = "";
    beginCss[constTop] = "";
    beginCss[constOverflow] = constHidden;
        
    endCss = {};
    endCss[constOverflow] = "";

    initializePrefix = ->
        baseClassName = prefix + "-fill";
        fillerSelector = "." + baseClassName;
        widthElementsSelector = "." + baseClassName + "-left,." + baseClassName + "-right";
        heightElementsSelector = "." + baseClassName + "-top,." + baseClassName + "-bottom";
        elementsSelector = fillerSelector + "," + widthElementsSelector + "," + heightElementsSelector;
        selectorList = elementsSelector.replace(/\./g, "").split(",");
        return;

    getEdgeWidth = (element, edge) ->
       marginPadding = parseIntWithDefault(element.css("margin-#{edge}"), 0);
       marginPadding += parseIntWithDefault(element.css("padding-#{edge}"), 0);

       borderStyle = element.css("border-#{edge}-style");

       if borderStyle != "none" && borderStyle != "hidden"
           marginPadding += parseIntWithDefault(element.css("border-#{edge}-width"), 0);

       return marginPadding;

    parseIntWithDefault = (text, defaultValue) ->
        return replaceNaNWithX(parseInt(text, 10), defaultValue);

    replaceNaNWithX = (number, x) ->
        if (number == undefined || isNaN(number))
            return x;
        else
            return number;

    startsWith = (str1, str2) ->
        return String(str1).indexOf(String(str2)) == 0;

    px = (count) ->
        return count.toString() + "px";

    sortFunction = (left,right) ->
        leftElement = $(left);
        rightElement = $(right);
        
        leftIndex = replaceNaNWithX( leftElement.data(prefix + "-sort-order"), 0) + (if leftElement.hasClass(baseClassName) then 10000 else 0);
        rightIndex = replaceNaNWithX( rightElement.data(prefix + "-sort-order"), 0) + (if rightElement.hasClass(baseClassName) then 10000 else 0);
               
        return if (leftIndex == rightIndex) then 0 else if (leftIndex > rightIndex) then 1 else -1;

    resizeScreen = ->
        if (isResizing || isSubtractJSPaused)
            return;

        isResizing = true;

        resetData();

        beforeResize();

        if !isSubtractJSPaused
            parents = $("." + prefix + "-parent:visible");
            parents.sort(sortFunction).each(subtractElementFromParentBox);
            
        isResizing = false;

        return;

    resetData = ->
        $(elementsSelector)
            .filter(":not(." + prefix + "-ignore):visible")
            .addClass(prefix + "-subtract")
            .css({
                "position": "absolute",
                "display": "block"
            });

        $("." + prefix + "-subtract:visible").each( ->
            self = $(this);
            parent = self.offsetParent();

            myParentId = parent.data(prefix + "-id");

            if (myParentId == undefined)
                if (!parent.hasClass(prefix + "-subtract"))
                    parent.addClass(prefix + "-parent");

                parent.data(prefix + "-id", nextParentId);

                myParentId = nextParentId;

                nextParentId++;

            self.addClass(prefix + "-parent-" + myParentId);
        );
        return;
        
    getFirstMatchingClass = (thisElement,nameStartsWith) ->
        result = "";
        classList = getClassList(thisElement);
        classList.forEach((item) ->
            if startsWith(item,nameStartsWith)
                result = item;
                return;
        );
        return result;
        
    subtractChildElementsFromParentBox = (element) ->
        parentClassSelector = "." + prefix + "-parent-" + element.data(prefix + "-id") + ":visible";

        element.css("overflow","hidden");
        setParentData(element,0,0,element.innerWidth(),element.innerHeight());

        selectedElements = element.find(parentClassSelector);
        selectedElements.sort(sortFunction).each(subtractElementFromParentBox);
        return;


    subtractElementFromParentBox = ->
        self = $(this);
 
        if (self.hasClass(prefix + "-ignore"))
            return;

        if (!self.hasClass(prefix + "-subtract"))
            if (!self.hasClass(prefix + "-ignore-children"))
                subtractChildElementsFromParentBox(self);
            self.css(endCss);

            return;

        parent = self.offsetParent();
        
        parentTop = parent.data(prefix + "-top");
        parentLeft = parent.data(prefix + "-left");
        parentWidth = parent.data(prefix + "-width");
        parentHeight = parent.data(prefix + "-height");
        
        self.css(beginCss);        
        parent.css(constOverflow,constHidden);

        adjustHeight = false;
        adjustWidth = false;
        adjustBoxModel = self.hasClass(prefix + "-margin-box-model");

        isWidthStyleFixed = true;
        isHeightStyleFixed = true;
        verticalAlign = "top";
        horizontalAlign = "left";

        fillClass = getFirstMatchingClass(self,baseClassName).replace(baseClassName,"").replace("-","");

        switch(fillClass)
            when ""
                isWidthStyleFixed = false;
                isHeightStyleFixed = false;
            when "left", "right"
                horizontalAlign = fillClass;
                isHeightStyleFixed = false;
                adjustWidth = adjustBoxModel;
            when "top", "bottom"
                verticalAlign = fillClass;
                isWidthStyleFixed = false;
                adjustHeight = adjustBoxModel;

        if (isWidthStyleFixed && isHeightStyleFixed)
            throw "SubtractJS: No element may have both fixed height and fixed width";

        if (adjustHeight)
            newHeight = self.height() - getEdgeWidth(self, "top") - getEdgeWidth(self, "bottom");
            self.css(constHeight,px(newHeight));

        if (adjustWidth)
            newWidth = self.width() - getEdgeWidth(self, "left") - getEdgeWidth(self, "right");
            self.css(constWidth,px(newWidth));

        myWidth = self.outerWidth(true);
        myHeight = self.outerHeight(true);

        if (isWidthStyleFixed)
            if (horizontalAlign == "left")
                self.css("left", px(parentLeft));

                parentLeft += myWidth;
                parentWidth -= myWidth;
            else
                self.css("left", px(parentWidth + parentLeft - myWidth));

                parentWidth -= myWidth;
                if (parentWidth < 0)
                    parentWidth = 0;

        else
            newWidth = parentWidth - (getEdgeWidth(self, "left") + getEdgeWidth(self, "right"));

            self.css({
                "left": px(parentLeft),
                "width": px(newWidth)
            });

        if (isHeightStyleFixed)
            if (verticalAlign == "top")
                self.css({
                    "top": px(parentTop)
                });

                parentTop += myHeight;
                parentHeight -= myHeight;
            else
                self.css({
                    "top": px(parentTop + parentHeight - myHeight)
                });

                parentHeight -= myHeight;
                if (parentHeight < 0)
                    parentHeight = 0;
        else
            newHeight = parentHeight - (getEdgeWidth(self, "top") + getEdgeWidth(self, "bottom"));

            self.css({
                "top": px(parentTop),
                "height": px(newHeight)
            });

        setParentData(parent,parentTop,parentLeft,parentWidth,parentHeight);

        if (!(self.data(prefix + "-id") == undefined && !self.hasClass(prefix + "-ignore-children")))
            subtractChildElementsFromParentBox(self);
        self.css(endCss);
        parent.css(endCss);
        return;
    
    setParentData = (element,top,left,width,height) ->    
        element.data(prefix + "-top", top);
        element.data(prefix + "-left", left);
        element.data(prefix + "-width", width);
        element.data(prefix + "-height", height);

    clearSubtractJS = (selector, fullClear) ->
        elements = $(selector);
        
        elements.each(->
            self = $(this);
            clearElement(self, fullClear);
            return;
        );

        return;
        
    getClassList = (thisElement) ->
        classAttr = thisElement.attr('class') || "";
        classAttr.split(/\s+/);

    clearElement = (thisElement, fullClear) ->
        if (fullClear)
            classes = getClassList(thisElement);

            $.each(classes, (item) ->
                if (startsWith(item, prefix))
                    thisElement.removeClass(item);
            );

        thisElement
            .css({
                "width": "",
                "height": "",
                "left": "",
                "top": "",
                "right": "",
                "bottom": "",
                "display": "",
                "position": ""
            });
        return;

    ###
     http://unscriptable.com/2009/03/20/debouncing-javascript-methods/
    ###
    debounce = (func, threshold, execAsap) ->
        timeout = 0;

        return -> 
            obj = this
            args = arguments;
            delayed = () ->
                if (!execAsap)
                    func.apply(obj, args);
                timeout = null;
                return;

            if (timeout)
                clearTimeout(timeout);
            else
                if (execAsap)
                    func.apply(obj, args);

            timeout = setTimeout(delayed, threshold || 50);
            return;

    initializeScreen = ->
        initializePrefix();

        isInitialized = true;

        if (doBodyInitialize)
            $("html,body").css({
                "height": "100%",
                "width": "100%",
                "margin": "0",
                "padding": "0",
                "border": "0px none"
            });

        resizeScreen();
        $(window).resize(debounce(resizeScreen));
        return;

    if ($)
        $(initializeScreen);
    else
        throw "jQuery is not available in SubtractJS";

    return {
        Initialize: ->
            initializeScreen();
            return this;
        ,

        Pause: ->
            isSubtractJSPaused = true;
            return this;
        ,

        Unpause: ->
            isSubtractJSPaused = false;
            return this;
        ,

        IsPaused: ->
            return isSubtractJSPaused;
        ,
        
        SetBodyNoInit: ->
            doBodyInitialize = false;
            return;
        ,

        SetPrefix: (newPrefix) ->
            if (isInitialized)
                throw "SetPrefix can only be called prior to initialization.";
            prefix = newPrefix;
            return this;
        ,

        ClearSubtractJS: (selector, fullClear) ->
            ### <signature>
            ///   <summary>Reset the properties that SubtractJS manipulates</summary>
            ///   <param name="selector" type="string"></param>
            ///   <param name="fullClear" type="Boolean"></param>
            /// </signature>
            ###
            clearSubtractJS(selector, fullClear);
            return this;
        ,

        BeforeResize: (beforeResizeFunction) ->
            ### <signature>
            ///   <summary>Pass in a function to execute before resizing</summary>
            ///   <param name="beforeResizeFunction" type="Function"></param>
            /// </signature>
            ###
            beforeResize = beforeResizeFunction;
            return this;
        ,

        UpdateLayout: ->
            ### <signature>
            ///   <summary>Call the SubtractJS resize event.</summary>
            /// </signature>
            ###
            resizeScreen();
            return this;        
        }
    return;
)(jQuery);
